/**
 * Copyright (c) iwindplus Technologies Co., Ltd.2024-2030, All rights reserved.
 */

package com.iwindplus.mgt.server.service.power.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.CharSequenceUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.PageDTO;
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
import com.github.yulichang.repository.JoinCrudRepository;
import com.iwindplus.boot.domain.constant.CommonConstant.SymbolConstant;
import com.iwindplus.boot.domain.enums.BizCodeEnum;
import com.iwindplus.boot.domain.enums.EnableStatusEnum;
import com.iwindplus.boot.domain.exception.BizException;
import com.iwindplus.boot.redis.manager.SerialNumManager;
import com.iwindplus.mgt.domain.dto.power.ResourceDTO;
import com.iwindplus.mgt.domain.dto.power.ResourceEditDTO;
import com.iwindplus.mgt.domain.dto.power.ResourceSaveDTO;
import com.iwindplus.mgt.domain.dto.power.ResourceSearchDTO;
import com.iwindplus.mgt.domain.enums.MgtCodeEnum;
import com.iwindplus.mgt.domain.enums.ResourceTypeEnum;
import com.iwindplus.mgt.domain.vo.power.ResourceBaseCheckedVO;
import com.iwindplus.mgt.domain.vo.power.ResourceBaseExtendVO;
import com.iwindplus.mgt.domain.vo.power.ResourceBaseVO;
import com.iwindplus.mgt.domain.vo.power.ResourceExtendVO;
import com.iwindplus.mgt.domain.vo.power.ResourceGroupVO;
import com.iwindplus.mgt.domain.vo.power.ResourcePageVO;
import com.iwindplus.mgt.server.mapper.power.ResourceMapper;
import com.iwindplus.mgt.server.model.power.ResourceDO;
import com.iwindplus.mgt.server.service.power.ResourceService;
import com.iwindplus.mgt.server.service.power.RoleResourceService;
import com.iwindplus.mgt.server.service.system.ServerService;
import jakarta.annotation.Resource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.utils.Lists;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * 资源业务层接口实现类.
 *
 * @author zengdegui
 * @since 2018/9/1
 */
@Slf4j
@Service
@CacheConfig(cacheNames = {"resource"})
@Transactional(rollbackFor = Exception.class)
public class ResourceServiceImpl extends JoinCrudRepository<ResourceMapper, ResourceDO> implements ResourceService {

    @Resource
    private RoleResourceService roleResourceService;

    @Resource
    private SerialNumManager serialNumManager;

    @Resource
    private ServerService serverService;

    @Caching(
        evict = {
            @CacheEvict(cacheNames = {"resource"}, allEntries = true),
            @CacheEvict(cacheNames = {"server"}, allEntries = true),
            @CacheEvict(cacheNames = {"menu"}, allEntries = true),
        }
    )
    @Override
    public boolean save(ResourceSaveDTO entity) {
        entity.setStatus(EnableStatusEnum.ENABLE);
        entity.setBuildInFlag(Boolean.FALSE);
        this.getNameIsExist(entity.getName().trim(), entity.getMenuId());
        entity.setSeq(this.getNextSeq(entity.getMenuId(), entity.getType()));
        if (CharSequenceUtil.isBlank(entity.getCode())) {
            final String key = new StringBuilder(entity.getType().name().toLowerCase()).append(SymbolConstant.UNDERLINE).toString();
            entity.setCode(this.serialNumManager.getSerialNum(key));
        } else {
            this.checkCode(entity);
            this.getCodeIsExist(entity.getCode());
        }
        this.getApiUrlIsExist(entity.getApiUrl().trim(), entity.getMenuId());
        final ResourceDO model = BeanUtil.copyProperties(entity, ResourceDO.class);
        super.save(model);
        entity.setId(model.getId());
        return Boolean.TRUE;
    }

    @Caching(
        evict = {
            @CacheEvict(cacheNames = {"resource"}, allEntries = true),
            @CacheEvict(cacheNames = {"server"}, allEntries = true),
            @CacheEvict(cacheNames = {"menu"}, allEntries = true),
        }
    )
    @Override
    public boolean removeByIds(List<Long> ids) {
        List<ResourceDO> list = super.listByIds(ids);
        if (CollUtil.isEmpty(list)) {
            throw new BizException(BizCodeEnum.DATA_NOT_EXIST);
        }
        boolean match = list.stream().anyMatch(ResourceDO::getBuildInFlag);
        if (Boolean.TRUE.equals(match)) {
            throw new BizException(BizCodeEnum.HAS_BUILD_IN_DATA);
        }
        this.roleResourceService.removeByResourceIds(ids);
        super.removeByIds(ids);
        return Boolean.TRUE;
    }

    @Caching(
        evict = {
            @CacheEvict(cacheNames = {"resource"}, allEntries = true),
            @CacheEvict(cacheNames = {"server"}, allEntries = true),
            @CacheEvict(cacheNames = {"menu"}, allEntries = true),
        }
    )
    @Override
    public boolean edit(ResourceEditDTO entity) {
        ResourceDO data = super.getById(entity.getId());
        if (Objects.isNull(data)) {
            throw new BizException(BizCodeEnum.DATA_NOT_EXIST);
        }
        if (Boolean.TRUE.equals(data.getBuildInFlag())) {
            throw new BizException(BizCodeEnum.BUILD_IN_DATA_NOT_OPERATE);
        }
        if (CharSequenceUtil.isNotBlank(entity.getName()) && !CharSequenceUtil.equals(data.getName(), entity.getName().trim())) {
            this.getNameIsExist(entity.getName().trim(), data.getMenuId());
        }
        if (Objects.isNull(entity.getVersion())) {
            entity.setVersion(data.getVersion());
        }
        if (CharSequenceUtil.isNotBlank(entity.getCode()) && !CharSequenceUtil.equals(data.getCode(), entity.getCode().trim())) {
            this.checkCode(entity);
            this.getCodeIsExist(entity.getCode());
        }
        if (CharSequenceUtil.isNotBlank(entity.getApiUrl()) && !CharSequenceUtil.equals(data.getApiUrl(), entity.getApiUrl().trim())) {
            this.getApiUrlIsExist(entity.getApiUrl().trim(), data.getMenuId());
        }
        final ResourceDO model = BeanUtil.copyProperties(entity, ResourceDO.class);
        super.updateById(model);
        return Boolean.TRUE;
    }

    @Caching(
        evict = {
            @CacheEvict(cacheNames = {"resource"}, allEntries = true),
            @CacheEvict(cacheNames = {"server"}, allEntries = true),
            @CacheEvict(cacheNames = {"menu"}, allEntries = true),
        }
    )
    @Override
    public boolean editStatus(Long id, EnableStatusEnum status) {
        ResourceDO data = super.getById(id);
        if (Objects.isNull(data)) {
            throw new BizException(BizCodeEnum.DATA_NOT_EXIST);
        }
        if (Boolean.TRUE.equals(data.getBuildInFlag())) {
            throw new BizException(BizCodeEnum.BUILD_IN_DATA_NOT_OPERATE);
        }
        if (status.equals(data.getStatus())) {
            throw new BizException(BizCodeEnum.ALREADY_OPERATED);
        }
        ResourceDO param = new ResourceDO();
        param.setId(id);
        param.setStatus(status);
        param.setVersion(data.getVersion());
        super.updateById(param);
        return Boolean.TRUE;
    }

    @Caching(
        evict = {
            @CacheEvict(cacheNames = {"resource"}, allEntries = true),
            @CacheEvict(cacheNames = {"server"}, allEntries = true),
            @CacheEvict(cacheNames = {"menu"}, allEntries = true),
        }
    )
    @Override
    public boolean editBuildIn(Long id, Boolean buildInFlag) {
        ResourceDO data = super.getById(id);
        if (Objects.isNull(data)) {
            throw new BizException(BizCodeEnum.DATA_NOT_EXIST);
        }
        if (buildInFlag.equals(data.getBuildInFlag())) {
            throw new BizException(BizCodeEnum.ALREADY_OPERATED);
        }
        ResourceDO param = new ResourceDO();
        param.setId(id);
        param.setBuildInFlag(buildInFlag);
        param.setVersion(data.getVersion());
        super.updateById(param);
        return Boolean.TRUE;
    }

    @Override
    public IPage<ResourcePageVO> page(ResourceSearchDTO entity) {
        final PageDTO<ResourcePageVO> page = new PageDTO<>(entity.getCurrent(), entity.getSize());
        page.setOptimizeCountSql(Boolean.FALSE);
        page.setOptimizeJoinOfCountSql(Boolean.FALSE);
        return super.baseMapper.selectPageByCondition(page, entity);
    }

    @Cacheable(key = "#root.methodName + '_' + #p0 + '_' + #p1", condition = "#p0 != null && #p1 != null", unless = "#result == null")
    @Override
    public List<ResourceBaseVO> listButtonCheckedByUserId(Long orgId, Long userId) {
        final List<ResourceBaseExtendVO> list = super.baseMapper.selectListCheckedByUserId(orgId, userId, Arrays.asList(ResourceTypeEnum.BUTTON));
        if (CollUtil.isEmpty(list)) {
            return null;
        }
        return list.stream()
            .map(m -> new ResourceBaseVO(m.getId(), m.getCode(), m.getName()))
            .sorted(Comparator.comparing(ResourceBaseVO::getName))
            .collect(Collectors.toCollection(ArrayList::new));
    }

    @Cacheable(key = "#root.methodName + '_' + #p0 + '_' + #p1", condition = "#p0 != null && #p1 != null", unless = "#result == null")
    @Override
    public List<String> listApiCheckedByUserId(Long orgId, Long userId) {
        List<String> resultList = new ArrayList<>(10);
        final List<ResourceBaseExtendVO> list = super.baseMapper.selectListCheckedByUserId(orgId, userId, null);
        if (CollUtil.isEmpty(list)) {
            return null;
        }
        list.stream().filter(m -> CharSequenceUtil.isNotBlank(m.getApiUrl())).forEach(data -> {
            final String contextPath = this.serverService.getContextPath(data.getServerPredicates());
            final String path = String.format("%s%s", contextPath, data.getApiUrl());
            resultList.add(path);
        });
        return resultList;
    }

    @Cacheable(key = "#root.methodName + '_' + #p0 + '_' + #p1", condition = "#p0 != null", unless = "#result == null")
    @Override
    public List<ResourceGroupVO> listByRoleId(Long orgId, Long roleId) {
        List<ResourceGroupVO> resultList = Lists.newArrayList();
        List<ResourceBaseCheckedVO> allList = super.baseMapper.selectListEnabled();
        if (CollUtil.isEmpty(allList)) {
            return null;
        }
        if (Objects.nonNull(roleId)) {
            List<ResourceBaseCheckedVO> checkedList = super.baseMapper.selectListByRoleId(orgId, roleId);
            if (CollUtil.isNotEmpty(checkedList)) {
                allList = this.listWithChecked(allList, checkedList);
            }
        }
        final Map<Long, List<ResourceBaseCheckedVO>> groupServerNameMap = this.getGroupServerMap(allList);
        if (MapUtil.isEmpty(groupServerNameMap)) {
            return null;
        }
        for (Map.Entry<Long, List<ResourceBaseCheckedVO>> groupServerNameMapEntry : groupServerNameMap.entrySet()) {
            List<ResourceGroupVO.MenuBaseVO> menus = new ArrayList<>(10);
            final List<ResourceBaseCheckedVO> groupServerNameList = groupServerNameMapEntry.getValue();
            final ResourceBaseCheckedVO server = groupServerNameList.get(0);
            Map<Long, List<ResourceBaseCheckedVO>> groupMenuNameMap = this.getGroupMenuMap(groupServerNameList);
            if (MapUtil.isNotEmpty(groupMenuNameMap)) {
                for (Map.Entry<Long, List<ResourceBaseCheckedVO>> groupMenuNameMapEntry : groupMenuNameMap.entrySet()) {
                    this.buildMenus(menus, groupMenuNameMapEntry);
                }
            }
            boolean checked = false;
            if (CollUtil.isNotEmpty(menus)) {
                Collections.sort(menus, Comparator.comparing(ResourceGroupVO.MenuBaseVO::getSeq));
                checked = menus.stream().anyMatch(ResourceGroupVO.MenuBaseVO::getChecked);
            }
            ResourceGroupVO builder = ResourceGroupVO.builder()
                .checked(checked)
                .id(server.getServerId())
                .name(server.getServerName())
                .code(server.getServerRouteId())
                .seq(server.getServerSeq())
                .menus(menus)
                .build();
            resultList.add(builder);
        }
        return resultList;
    }

    @Cacheable(key = "#root.methodName + '_' + #p0", condition = "#p0 != null", unless = "#result == null")
    @Override
    public ResourceExtendVO getDetailExtend(Long id) {
        ResourceDO data = super.getById(id);
        if (Objects.isNull(data)) {
            throw new BizException(BizCodeEnum.DATA_NOT_EXIST);
        }
        ResourceExtendVO result = BeanUtil.copyProperties(data, ResourceExtendVO.class);
        return result;
    }

    private List<ResourceBaseCheckedVO> listWithChecked(List<ResourceBaseCheckedVO> allList,
        List<ResourceBaseCheckedVO> checkedList) {
        return allList.stream().peek(map -> checkedList.stream()
                .filter(m -> Objects.equals(m.getId(), map.getId())).forEach(m -> map.setChecked(m.getChecked())))
            .collect(Collectors.toCollection(ArrayList::new));
    }

    private Map<Long, List<ResourceBaseCheckedVO>> getGroupServerMap(List<ResourceBaseCheckedVO> list) {
        return list.stream().sorted(Comparator.comparing(ResourceBaseCheckedVO::getServerSeq))
            .collect(Collectors.groupingBy(ResourceBaseCheckedVO::getServerId));
    }

    private Map<Long, List<ResourceBaseCheckedVO>> getGroupMenuMap(List<ResourceBaseCheckedVO> list) {
        return list.stream().sorted(Comparator.comparing(ResourceBaseCheckedVO::getMenuSeq))
            .collect(Collectors.groupingBy(ResourceBaseCheckedVO::getMenuId));
    }

    private void buildMenus(List<ResourceGroupVO.MenuBaseVO> menus,
        Map.Entry<Long, List<ResourceBaseCheckedVO>> groupMenuNameMapEntry) {
        List<ResourceBaseCheckedVO> groupMenuNameList = groupMenuNameMapEntry.getValue();
        if (CollUtil.isEmpty(groupMenuNameList)) {
            return;
        }
        final ResourceBaseCheckedVO menu = groupMenuNameList.get(0);
        List<ResourceGroupVO.ResourceBaseVO> resources = groupMenuNameList.stream()
            .sorted(Comparator.comparing(ResourceBaseCheckedVO::getType))
            .map(m -> ResourceGroupVO.ResourceBaseVO
                .builder()
                .checked(m.getChecked())
                .id(m.getId())
                .name(m.getName())
                .code(m.getCode())
                .seq(m.getSeq())
                .type(m.getType())
                .build())
            .collect(Collectors.toCollection(ArrayList::new));
        boolean checked = resources.stream().anyMatch(ResourceGroupVO.ResourceBaseVO::getChecked);
        ResourceGroupVO.MenuBaseVO build = ResourceGroupVO.MenuBaseVO.builder()
            .checked(checked)
            .id(menu.getMenuId())
            .name(menu.getMenuName())
            .code(menu.getMenuCode())
            .seq(menu.getMenuSeq())
            .resources(resources)
            .build();
        menus.add(build);
    }

    private void checkCode(ResourceDTO entity) {
        if (ResourceTypeEnum.BUTTON.equals(entity.getType())) {
            if (!entity.getCode().startsWith(ResourceTypeEnum.BUTTON.name().toLowerCase())) {
                throw new BizException(MgtCodeEnum.BUTTON_PREFIX_ERROR);
            }
        } else if (ResourceTypeEnum.API.equals(entity.getType())) {
            if (!entity.getCode().startsWith(ResourceTypeEnum.API.name().toLowerCase())) {
                throw new BizException(MgtCodeEnum.API_PREFIX_ERROR);
            }
        }
    }

    private void getNameIsExist(String name, Long menuId) {
        final LambdaQueryWrapper<ResourceDO> queryWrapper = Wrappers.lambdaQuery(ResourceDO.class)
            .eq(ResourceDO::getMenuId, menuId)
            .eq(ResourceDO::getName, name);
        boolean result = SqlHelper.retBool(super.count(queryWrapper));
        if (Boolean.TRUE.equals(result)) {
            throw new BizException(MgtCodeEnum.NAME_EXIST);
        }
    }

    private void getApiUrlIsExist(String apiUrl, Long menuId) {
        final LambdaQueryWrapper<ResourceDO> queryWrapper = Wrappers.lambdaQuery(ResourceDO.class)
            .eq(ResourceDO::getMenuId, menuId)
            .eq(ResourceDO::getApiUrl, apiUrl);
        boolean result = SqlHelper.retBool(super.count(queryWrapper));
        if (Boolean.TRUE.equals(result)) {
            throw new BizException(MgtCodeEnum.API_URL_EXIST);
        }
    }

    private void getCodeIsExist(String code) {
        final LambdaQueryWrapper<ResourceDO> queryWrapper = Wrappers.lambdaQuery(ResourceDO.class)
            .eq(ResourceDO::getCode, code);
        boolean result = SqlHelper.retBool(super.count(queryWrapper));
        if (result) {
            throw new BizException(MgtCodeEnum.CODE_EXIST);
        }
    }

    private Integer getNextSeq(Long menuId, ResourceTypeEnum type) {
        QueryWrapper<ResourceDO> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(ResourceDO::getMenuId, menuId);
        queryWrapper.lambda().eq(ResourceDO::getType, type);
        queryWrapper.select("max(seq) as seq");
        Function<Object, Integer> function = val -> Integer.valueOf(val.toString());
        Integer data = super.getObj(queryWrapper, function);
        return Optional.ofNullable(data).map(x -> x + 1).orElse(1);
    }

}
